agora inbox for pgsql-hackers@postgresql.org
help / color / mirror / Atom feed[PATCH 2/5] Use IndexOrderByDistance in SP-GiST user functions
19+ messages / 9 participants
[nested] [flat]
* [PATCH 2/5] Use IndexOrderByDistance in SP-GiST user functions
@ 2019-09-16 12:36 Nikita Glukhov <n.gluhov@postgrespro.ru>
0 siblings, 0 replies; 19+ messages in thread
From: Nikita Glukhov @ 2019-09-16 12:36 UTC (permalink / raw)
---
src/backend/access/spgist/spgkdtreeproc.c | 2 +-
src/backend/access/spgist/spgproc.c | 9 +--
src/backend/access/spgist/spgquadtreeproc.c | 2 +-
src/backend/access/spgist/spgscan.c | 96 ++++++++++++++++++-----------
src/backend/utils/adt/geo_spgist.c | 18 +++---
src/include/access/spgist.h | 4 +-
src/include/access/spgist_private.h | 14 +++--
7 files changed, 88 insertions(+), 57 deletions(-)
diff --git a/src/backend/access/spgist/spgkdtreeproc.c b/src/backend/access/spgist/spgkdtreeproc.c
index 9d479fe..66ab1ac 100644
--- a/src/backend/access/spgist/spgkdtreeproc.c
+++ b/src/backend/access/spgist/spgkdtreeproc.c
@@ -271,7 +271,7 @@ spg_kd_inner_consistent(PG_FUNCTION_ARGS)
BOX infArea;
BOX *area;
- out->distances = (double **) palloc(sizeof(double *) * in->nNodes);
+ out->distances = palloc(sizeof(out->distances[0]) * in->nNodes);
out->traversalValues = (void **) palloc(sizeof(void *) * in->nNodes);
if (in->level == 0)
diff --git a/src/backend/access/spgist/spgproc.c b/src/backend/access/spgist/spgproc.c
index 688638a..41203ed 100644
--- a/src/backend/access/spgist/spgproc.c
+++ b/src/backend/access/spgist/spgproc.c
@@ -59,20 +59,21 @@ point_box_distance(Point *point, BOX *box)
* is expected to be point, non-leaf key is expected to be box. Scan key
* arguments are expected to be points.
*/
-double *
+IndexOrderByDistance *
spg_key_orderbys_distances(Datum key, bool isLeaf,
ScanKey orderbys, int norderbys)
{
int sk_num;
- double *distances = (double *) palloc(norderbys * sizeof(double)),
+ IndexOrderByDistance *distances = palloc(sizeof(distances[0]) * norderbys),
*distance = distances;
for (sk_num = 0; sk_num < norderbys; ++sk_num, ++orderbys, ++distance)
{
Point *point = DatumGetPointP(orderbys->sk_argument);
- *distance = isLeaf ? point_point_distance(point, DatumGetPointP(key))
- : point_box_distance(point, DatumGetBoxP(key));
+ distance->isnull = false;
+ distance->value = isLeaf ? point_point_distance(point, DatumGetPointP(key))
+ : point_box_distance(point, DatumGetBoxP(key));
}
return distances;
diff --git a/src/backend/access/spgist/spgquadtreeproc.c b/src/backend/access/spgist/spgquadtreeproc.c
index e50108e..3f59bea 100644
--- a/src/backend/access/spgist/spgquadtreeproc.c
+++ b/src/backend/access/spgist/spgquadtreeproc.c
@@ -247,7 +247,7 @@ spg_quad_inner_consistent(PG_FUNCTION_ARGS)
*/
if (in->norderbys > 0)
{
- out->distances = (double **) palloc(sizeof(double *) * in->nNodes);
+ out->distances = palloc(sizeof(out->distances[0]) * in->nNodes);
out->traversalValues = (void **) palloc(sizeof(void *) * in->nNodes);
if (in->level == 0)
diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c
index 053bf09..67d23f2 100644
--- a/src/backend/access/spgist/spgscan.c
+++ b/src/backend/access/spgist/spgscan.c
@@ -28,7 +28,8 @@
typedef void (*storeRes_func) (SpGistScanOpaque so, ItemPointer heapPtr,
Datum leafValue, bool isNull, bool recheck,
- bool recheckDistances, double *distances);
+ bool recheckDistances,
+ IndexOrderByDistance *distances);
/*
* Pairing heap comparison function for the SpGistSearchItem queue.
@@ -56,16 +57,25 @@ pairingheap_SpGistSearchItem_cmp(const pairingheap_node *a,
else
{
/* Order according to distance comparison */
- for (i = 0; i < so->numberOfOrderBys; i++)
+ for (i = 0; i < so->numberOfNonNullOrderBys; i++)
{
- if (isnan(sa->distances[i]) && isnan(sb->distances[i]))
+ if (sa->distances[i].isnull)
+ {
+ if (!sb->distances[i].isnull)
+ return -1;
+ continue;
+ }
+ else if (sb->distances[i].isnull)
+ return 1;
+
+ if (isnan(sa->distances[i].value) && isnan(sb->distances[i].value))
continue; /* NaN == NaN */
- if (isnan(sa->distances[i]))
+ if (isnan(sa->distances[i].value))
return -1; /* NaN > number */
- if (isnan(sb->distances[i]))
+ if (isnan(sb->distances[i].value))
return 1; /* number < NaN */
- if (sa->distances[i] != sb->distances[i])
- return (sa->distances[i] < sb->distances[i]) ? 1 : -1;
+ if (sa->distances[i].value != sb->distances[i].value)
+ return (sa->distances[i].value < sb->distances[i].value) ? 1 : -1;
}
}
@@ -103,7 +113,8 @@ spgAddSearchItemToQueue(SpGistScanOpaque so, SpGistSearchItem *item)
}
static SpGistSearchItem *
-spgAllocSearchItem(SpGistScanOpaque so, bool isnull, double *distances)
+spgAllocSearchItem(SpGistScanOpaque so, bool isnull,
+ IndexOrderByDistance *distances)
{
/* allocate distance array only for non-NULL items */
SpGistSearchItem *item =
@@ -327,15 +338,17 @@ spgbeginscan(Relation rel, int keysz, int orderbysz)
palloc(sizeof(int) * scan->numberOfOrderBys);
/* These arrays have constant contents, so we can fill them now */
- so->zeroDistances = (double *)
- palloc(sizeof(double) * scan->numberOfOrderBys);
- so->infDistances = (double *)
- palloc(sizeof(double) * scan->numberOfOrderBys);
+ so->zeroDistances =
+ palloc(sizeof(so->zeroDistances[0]) * scan->numberOfOrderBys);
+ so->infDistances =
+ palloc(sizeof(so->infDistances[0]) * scan->numberOfOrderBys);
for (i = 0; i < scan->numberOfOrderBys; i++)
{
- so->zeroDistances[i] = 0.0;
- so->infDistances[i] = get_float8_infinity();
+ so->zeroDistances[i].value = 0.0;
+ so->zeroDistances[i].isnull = false;
+ so->infDistances[i].value = get_float8_infinity();
+ so->infDistances[i].isnull = false;
}
scan->xs_orderbyvals = (Datum *)
@@ -440,7 +453,7 @@ spgendscan(IndexScanDesc scan)
static SpGistSearchItem *
spgNewHeapItem(SpGistScanOpaque so, int level, ItemPointer heapPtr,
Datum leafValue, bool recheck, bool recheckDistances,
- bool isnull, double *distances)
+ bool isnull, IndexOrderByDistance *distances)
{
SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
@@ -470,7 +483,7 @@ spgLeafTest(SpGistScanOpaque so, SpGistSearchItem *item,
bool *reportedSome, storeRes_func storeRes)
{
Datum leafValue;
- double *distances;
+ IndexOrderByDistance *distances;
bool result;
bool recheck;
bool recheckDistances;
@@ -580,7 +593,7 @@ spgMakeInnerItem(SpGistScanOpaque so,
SpGistSearchItem *parentItem,
SpGistNodeTuple tuple,
spgInnerConsistentOut *out, int i, bool isnull,
- double *distances)
+ IndexOrderByDistance *distances)
{
SpGistSearchItem *item = spgAllocSearchItem(so, isnull, distances);
@@ -664,7 +677,7 @@ spgInnerTest(SpGistScanOpaque so, SpGistSearchItem *item,
{
int nodeN = out.nodeNumbers[i];
SpGistSearchItem *innerItem;
- double *distances;
+ IndexOrderByDistance *distances;
Assert(nodeN >= 0 && nodeN < nNodes);
@@ -878,7 +891,7 @@ redirect:
static void
storeBitmap(SpGistScanOpaque so, ItemPointer heapPtr,
Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
- double *distances)
+ IndexOrderByDistance *distances)
{
Assert(!recheckDistances && !distances);
tbm_add_tuples(so->tbm, heapPtr, 1, recheck);
@@ -905,7 +918,7 @@ spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm)
static void
storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
Datum leafValue, bool isnull, bool recheck, bool recheckDistances,
- double *nonNullDistances)
+ IndexOrderByDistance *nonNullDistances)
{
Assert(so->nPtrs < MaxIndexTuplesPerPage);
so->heapPtrs[so->nPtrs] = *heapPtr;
@@ -918,25 +931,38 @@ storeGettuple(SpGistScanOpaque so, ItemPointer heapPtr,
so->distances[so->nPtrs] = NULL;
else
{
- IndexOrderByDistance *distances =
- palloc(sizeof(distances[0]) * so->numberOfOrderBys);
- int i;
+ IndexOrderByDistance *distances;
+ Size size = sizeof(distances[0]) * so->numberOfOrderBys;
- for (i = 0; i < so->numberOfOrderBys; i++)
+ distances = palloc(size);
+
+ if (so->numberOfNonNullOrderBys >= so->numberOfOrderBys)
+ {
+ /*
+ * All distance keys are not NULL, so simply copy distance
+ * values.
+ */
+ memcpy(distances, nonNullDistances, size);
+ }
+ else
{
- int offset = so->nonNullOrderByOffsets[i];
+ int i;
- if (offset >= 0)
+ for (i = 0; i < so->numberOfOrderBys; i++)
{
- /* Copy non-NULL distance value */
- distances[i].value = nonNullDistances[offset];
- distances[i].isnull = false;
- }
- else
- {
- /* Set distance's NULL flag. */
- distances[i].value = 0.0;
- distances[i].isnull = true;
+ int offset = so->nonNullOrderByOffsets[i];
+
+ if (offset >= 0)
+ {
+ /* Copy non-NULL distance value */
+ distances[i] = nonNullDistances[offset];
+ }
+ else
+ {
+ /* Set distance's NULL flag. */
+ distances[i].value = 0.0;
+ distances[i].isnull = true;
+ }
}
}
diff --git a/src/backend/utils/adt/geo_spgist.c b/src/backend/utils/adt/geo_spgist.c
index 8e29770..448b574 100644
--- a/src/backend/utils/adt/geo_spgist.c
+++ b/src/backend/utils/adt/geo_spgist.c
@@ -580,24 +580,25 @@ spg_box_quad_inner_consistent(PG_FUNCTION_ARGS)
if (in->norderbys > 0 && in->nNodes > 0)
{
- double *distances = palloc(sizeof(double) * in->norderbys);
+ IndexOrderByDistance *distances = palloc(sizeof(distances[0]) * in->norderbys);
int j;
for (j = 0; j < in->norderbys; j++)
{
Point *pt = DatumGetPointP(in->orderbys[j].sk_argument);
- distances[j] = pointToRectBoxDistance(pt, rect_box);
+ distances[j].value = pointToRectBoxDistance(pt, rect_box);
+ distances[j].isnull = false;
}
- out->distances = (double **) palloc(sizeof(double *) * in->nNodes);
+ out->distances = palloc(sizeof(out->distances[0]) * in->nNodes);
out->distances[0] = distances;
for (i = 1; i < in->nNodes; i++)
{
- out->distances[i] = palloc(sizeof(double) * in->norderbys);
+ out->distances[i] = palloc(sizeof(distances[0]) * in->norderbys);
memcpy(out->distances[i], distances,
- sizeof(double) * in->norderbys);
+ sizeof(distances[0]) * in->norderbys);
}
}
@@ -622,7 +623,7 @@ spg_box_quad_inner_consistent(PG_FUNCTION_ARGS)
out->nodeNumbers = (int *) palloc(sizeof(int) * in->nNodes);
out->traversalValues = (void **) palloc(sizeof(void *) * in->nNodes);
if (in->norderbys > 0)
- out->distances = (double **) palloc(sizeof(double *) * in->nNodes);
+ out->distances = palloc(sizeof(out->distances[0]) * in->nNodes);
/*
* We switch memory context, because we want to allocate memory for new
@@ -703,7 +704,7 @@ spg_box_quad_inner_consistent(PG_FUNCTION_ARGS)
if (in->norderbys > 0)
{
- double *distances = palloc(sizeof(double) * in->norderbys);
+ IndexOrderByDistance *distances = palloc(sizeof(distances[0]) * in->norderbys);
int j;
out->distances[out->nNodes] = distances;
@@ -712,7 +713,8 @@ spg_box_quad_inner_consistent(PG_FUNCTION_ARGS)
{
Point *pt = DatumGetPointP(in->orderbys[j].sk_argument);
- distances[j] = pointToRectBoxDistance(pt, next_rect_box);
+ distances[j].value = pointToRectBoxDistance(pt, next_rect_box);
+ distances[j].isnull = false;
}
}
diff --git a/src/include/access/spgist.h b/src/include/access/spgist.h
index d787ab2..bcbcb1b 100644
--- a/src/include/access/spgist.h
+++ b/src/include/access/spgist.h
@@ -161,7 +161,7 @@ typedef struct spgInnerConsistentOut
int *levelAdds; /* increment level by this much for each */
Datum *reconstructedValues; /* associated reconstructed values */
void **traversalValues; /* opclass-specific traverse values */
- double **distances; /* associated distances */
+ IndexOrderByDistance **distances; /* associated distances */
} spgInnerConsistentOut;
/*
@@ -188,7 +188,7 @@ typedef struct spgLeafConsistentOut
Datum leafValue; /* reconstructed original data, if any */
bool recheck; /* set true if operator must be rechecked */
bool recheckDistances; /* set true if distances must be rechecked */
- double *distances; /* associated distances */
+ IndexOrderByDistance *distances; /* associated distances */
} spgLeafConsistentOut;
diff --git a/src/include/access/spgist_private.h b/src/include/access/spgist_private.h
index 3d322c9..1bfe7fa 100644
--- a/src/include/access/spgist_private.h
+++ b/src/include/access/spgist_private.h
@@ -145,11 +145,12 @@ typedef struct SpGistSearchItem
bool recheckDistances; /* distance recheck is needed */
/* array with numberOfOrderBys entries */
- double distances[FLEXIBLE_ARRAY_MEMBER];
+ IndexOrderByDistance distances[FLEXIBLE_ARRAY_MEMBER];
} SpGistSearchItem;
#define SizeOfSpGistSearchItem(n_distances) \
- (offsetof(SpGistSearchItem, distances) + sizeof(double) * (n_distances))
+ (offsetof(SpGistSearchItem, distances) + \
+ sizeof(IndexOrderByDistance) * (n_distances))
/*
* Private state of an index scan
@@ -182,8 +183,8 @@ typedef struct SpGistScanOpaqueData
FmgrInfo leafConsistentFn;
/* Pre-allocated workspace arrays: */
- double *zeroDistances;
- double *infDistances;
+ IndexOrderByDistance *zeroDistances;
+ IndexOrderByDistance *infDistances;
/* These fields are only used in amgetbitmap scans: */
TIDBitmap *tbm; /* bitmap being filled */
@@ -465,8 +466,9 @@ extern bool spgdoinsert(Relation index, SpGistState *state,
ItemPointer heapPtr, Datum datum, bool isnull);
/* spgproc.c */
-extern double *spg_key_orderbys_distances(Datum key, bool isLeaf,
- ScanKey orderbys, int norderbys);
+extern IndexOrderByDistance *spg_key_orderbys_distances(Datum key, bool isLeaf,
+ ScanKey orderbys,
+ int norderbys);
extern BOX *box_copy(BOX *orig);
#endif /* SPGIST_PRIVATE_H */
--
2.7.4
--------------5AE546B531D96C8320FA33AB--
^ permalink raw reply [nested|flat] 19+ messages in thread
* Available disk space per tablespace
@ 2025-03-13 18:10 Christoph Berg <myon@debian.org>
0 siblings, 2 replies; 19+ messages in thread
From: Christoph Berg @ 2025-03-13 18:10 UTC (permalink / raw)
To: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Hi,
I'm picking up a 5 year old patch again:
https://www.postgresql.org/message-id/flat/20191108132419.GG8017%40msg.df7cb.de
Users will be interested in knowing how much extra data they can load
into a database, but PG currently does not expose that number. This
patch introduces a new function pg_tablespace_avail() that takes a
tablespace name or oid, and returns the number of bytes "available"
there. This is the number without any reserved blocks (Unix, f_avail)
or available to the current user (Windows).
(This is not meant to replace a full-fledged OS monitoring system that
has much more numbers about disks and everything, it is filling a UX
gap.)
Compared to the last patch, this just returns a single number so it's
easier to use - total space isn't all that interesting, we just return
the number the user wants.
The free space is included in \db+ output:
postgres =# \db+
List of tablespaces
Name │ Owner │ Location │ Access privileges │ Options │ Size │ Free │ Description
────────────┼───────┼──────────┼───────────────────┼─────────┼─────────┼────────┼─────────────
pg_default │ myon │ │ ∅ │ ∅ │ 23 MB │ 538 GB │ ∅
pg_global │ myon │ │ ∅ │ ∅ │ 556 kB │ 538 GB │ ∅
spc │ myon │ /tmp/spc │ ∅ │ ∅ │ 0 bytes │ 31 GB │ ∅
(3 rows)
The patch has also been tested on Windows.
TODO: Figure out which systems need statfs() vs statvfs()
Christoph
Attachments:
[text/x-diff] 0001-Add-pg_tablespace_avail-functions.patch (7.6K, ../../Z9MfiPfZULuzsedO@msg.df7cb.de/2-0001-Add-pg_tablespace_avail-functions.patch)
download | inline diff:
From 455640e375e7142d4bef2e4f47f678e3712a5a27 Mon Sep 17 00:00:00 2001
From: Christoph Berg <myon@debian.org>
Date: Fri, 8 Nov 2019 14:12:35 +0100
Subject: [PATCH] Add pg_tablespace_avail() functions
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
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
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2025-03-14 02:14 Quan Zongliang <quanzongliang@yeah.net>
parent: Christoph Berg <myon@debian.org>
1 sibling, 1 reply; 19+ messages in thread
From: Quan Zongliang @ 2025-03-14 02:14 UTC (permalink / raw)
To: Christoph Berg <myon@debian.org>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On 2025/3/14 02:10, Christoph Berg wrote:
> Hi,
>
> I'm picking up a 5 year old patch again:
> https://www.postgresql.org/message-id/flat/20191108132419.GG8017%40msg.df7cb.de
>
> Users will be interested in knowing how much extra data they can load
> into a database, but PG currently does not expose that number. This
> patch introduces a new function pg_tablespace_avail() that takes a
> tablespace name or oid, and returns the number of bytes "available"
> there. This is the number without any reserved blocks (Unix, f_avail)
> or available to the current user (Windows).
>
> (This is not meant to replace a full-fledged OS monitoring system that
> has much more numbers about disks and everything, it is filling a UX
> gap.)
>
> Compared to the last patch, this just returns a single number so it's
> easier to use - total space isn't all that interesting, we just return
> the number the user wants.
>
> The free space is included in \db+ output:
>
> postgres =# \db+
> List of tablespaces
> Name │ Owner │ Location │ Access privileges │ Options │ Size │ Free │ Description
> ────────────┼───────┼──────────┼───────────────────┼─────────┼─────────┼────────┼─────────────
> pg_default │ myon │ │ ∅ │ ∅ │ 23 MB │ 538 GB │ ∅
> pg_global │ myon │ │ ∅ │ ∅ │ 556 kB │ 538 GB │ ∅
> spc │ myon │ /tmp/spc │ ∅ │ ∅ │ 0 bytes │ 31 GB │ ∅
> (3 rows)
>
> The patch has also been tested on Windows.
>
> TODO: Figure out which systems need statfs() vs statvfs()
>
I tested the patch under macos. Abnormal work:
List of tablespaces
Name | Owner | Location | Access privileges | Options | Size
| Free | Description
------------+--------+----------+-------------------+---------+--------+-------+-------------
pg_default | quanzl | | | | 23 MB
|23 TB |
pg_global | quanzl | | | | 556 kB
| 23 TB |
(2 rows)
Actually my disk is 1TB.
According to the statvfs documentation for macOS
f_frsize The size in bytes of the minimum unit of allocation on
this file system.
f_bsize The preferred length of I/O requests for files on this
file system.
I tweaked the code a little bit. See the attachment.
List of tablespaces
Name | Owner | Location | Access privileges | Options | Size
| Free | Description
------------+--------+----------+-------------------+---------+--------+--------+-------------
pg_default | quanzl | | | | 22 MB
| 116 GB |
pg_global | quanzl | | | | 556 kB
| 116 GB |
(2 rows)
In addition, many systems use 1000 as 1k to represent the storage size.
Shouldn't we consider this factor as well?
> Christoph
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 1c3810e1a04..c0758b9244f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30089,6 +30089,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..a2637953ce0 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,99 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+#if defined(__darwin__)
+ return fst.f_bavail * fst.f_frsize; /* available blocks times block size */
+#else
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif /* __darwin__ */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 890822eaf79..39c3b8c2552 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7683,6 +7683,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
Attachments:
[text/plain] 0002-Add-pg_tablespace_avail-functions.patch (6.9K, ../../2df8509c-e28a-467a-8f03-f3cfe812ed62@yeah.net/2-0002-Add-pg_tablespace_avail-functions.patch)
download | inline diff:
diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml
index 1c3810e1a04..c0758b9244f 100644
--- a/doc/src/sgml/func.sgml
+++ b/doc/src/sgml/func.sgml
@@ -30089,6 +30089,27 @@ postgres=# SELECT '0/0'::pg_lsn + pd.segment_number * ps.setting::int + :offset
</para></entry>
</row>
+ <row>
+ <entry role="func_table_entry"><para role="func_signature">
+ <indexterm>
+ <primary>pg_tablespace_avail</primary>
+ </indexterm>
+ <function>pg_tablespace_avail</function> ( <type>name</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para role="func_signature">
+ <function>pg_tablespace_avail</function> ( <type>oid</type> )
+ <returnvalue>bigint</returnvalue>
+ </para>
+ <para>
+ Returns the available disk space in the tablespace with the
+ specified name or OID. To use this function, you must
+ have <literal>CREATE</literal> privilege on the specified tablespace
+ or have privileges of the <literal>pg_read_all_stats</literal> role,
+ unless it is the default tablespace for the current database.
+ </para></entry>
+ </row>
+
<row>
<entry role="func_table_entry"><para role="func_signature">
<indexterm>
diff --git a/doc/src/sgml/ref/psql-ref.sgml b/doc/src/sgml/ref/psql-ref.sgml
index cedccc14129..9e1bec0b422 100644
--- a/doc/src/sgml/ref/psql-ref.sgml
+++ b/doc/src/sgml/ref/psql-ref.sgml
@@ -1492,7 +1492,7 @@ SELECT $1 \parse stmt1
If <literal>x</literal> is appended to the command name, the results
are displayed in expanded mode.
If <literal>+</literal> is appended to the command name, each tablespace
- is listed with its associated options, on-disk size, permissions and
+ is listed with its associated options, on-disk size and free disk space, permissions and
description.
</para>
</listitem>
diff --git a/src/backend/utils/adt/dbsize.c b/src/backend/utils/adt/dbsize.c
index 25865b660ef..a2637953ce0 100644
--- a/src/backend/utils/adt/dbsize.c
+++ b/src/backend/utils/adt/dbsize.c
@@ -12,6 +12,11 @@
#include "postgres.h"
#include <sys/stat.h>
+#ifdef WIN32
+#include <fileapi.h>
+#else
+#include <sys/statvfs.h>
+#endif
#include "access/htup_details.h"
#include "access/relation.h"
@@ -316,6 +321,99 @@ pg_tablespace_size_name(PG_FUNCTION_ARGS)
}
+/*
+ * Return available disk space of tablespace. Returns -1 if the tablespace
+ * directory cannot be found.
+ */
+static int64
+calculate_tablespace_avail(Oid tblspcOid)
+{
+ char tblspcPath[MAXPGPATH];
+ AclResult aclresult;
+#ifdef WIN32
+ ULARGE_INTEGER lpFreeBytesAvailable;
+#else
+ struct statvfs fst;
+#endif
+
+ /*
+ * User must have privileges of pg_read_all_stats or have CREATE privilege
+ * for target tablespace, either explicitly granted or implicitly because
+ * it is default for current database.
+ */
+ if (tblspcOid != MyDatabaseTableSpace &&
+ !has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS))
+ {
+ aclresult = object_aclcheck(TableSpaceRelationId, tblspcOid, GetUserId(), ACL_CREATE);
+ if (aclresult != ACLCHECK_OK)
+ aclcheck_error(aclresult, OBJECT_TABLESPACE,
+ get_tablespace_name(tblspcOid));
+ }
+
+ if (tblspcOid == DEFAULTTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "base");
+ else if (tblspcOid == GLOBALTABLESPACE_OID)
+ snprintf(tblspcPath, MAXPGPATH, "global");
+ else
+ snprintf(tblspcPath, MAXPGPATH, "%s/%u/%s", PG_TBLSPC_DIR, tblspcOid,
+ TABLESPACE_VERSION_DIRECTORY);
+
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL) == false)
+ return -1;
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
+ if (statvfs(tblspcPath, &fst) < 0)
+ return -1;
+
+#if defined(__darwin__)
+ return fst.f_bavail * fst.f_frsize; /* available blocks times block size */
+#else
+ return fst.f_bavail * fst.f_bsize; /* available blocks times block size */
+#endif /* __darwin__ */
+#endif
+}
+
+Datum
+pg_tablespace_avail_oid(PG_FUNCTION_ARGS)
+{
+ Oid tblspcOid = PG_GETARG_OID(0);
+ int64 avail;
+
+ /*
+ * Not needed for correctness, but avoid non-user-facing error message
+ * later if the tablespace doesn't exist.
+ */
+ if (!SearchSysCacheExists1(TABLESPACEOID, ObjectIdGetDatum(tblspcOid)))
+ ereport(ERROR,
+ errcode(ERRCODE_UNDEFINED_OBJECT),
+ errmsg("tablespace with OID %u does not exist", tblspcOid));
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+Datum
+pg_tablespace_avail_name(PG_FUNCTION_ARGS)
+{
+ Name tblspcName = PG_GETARG_NAME(0);
+ Oid tblspcOid = get_tablespace_oid(NameStr(*tblspcName), false);
+ int64 avail;
+
+ avail = calculate_tablespace_avail(tblspcOid);
+
+ if (avail < 0)
+ PG_RETURN_NULL();
+
+ PG_RETURN_INT64(avail);
+}
+
+
/*
* calculate size of (one fork of) a relation
*
diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c
index e6cf468ac9e..8c52a126ac1 100644
--- a/src/bin/psql/describe.c
+++ b/src/bin/psql/describe.c
@@ -241,10 +241,15 @@ describeTablespaces(const char *pattern, bool verbose)
printACLColumn(&buf, "spcacl");
appendPQExpBuffer(&buf,
",\n spcoptions AS \"%s\""
- ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\""
- ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"",
gettext_noop("Options"),
- gettext_noop("Size"),
+ gettext_noop("Size"));
+ if (pset.sversion >= 180000)
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(oid)) AS \"%s\"",
+ gettext_noop("Free"));
+ appendPQExpBuffer(&buf,
+ ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"",
gettext_noop("Description"));
}
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 890822eaf79..39c3b8c2552 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -7683,6 +7683,14 @@
descr => 'total disk space usage for the specified tablespace',
proname => 'pg_tablespace_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'name', prosrc => 'pg_tablespace_size_name' },
+{ oid => '6015',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'oid', prosrc => 'pg_tablespace_avail_oid' },
+{ oid => '6016',
+ descr => 'disk stats for the specified tablespace',
+ proname => 'pg_tablespace_avail', provolatile => 'v', prorettype => 'int8',
+ proargtypes => 'name', prosrc => 'pg_tablespace_avail_name' },
{ oid => '2324', descr => 'total disk space usage for the specified database',
proname => 'pg_database_size', provolatile => 'v', prorettype => 'int8',
proargtypes => 'oid', prosrc => 'pg_database_size_oid' },
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2025-03-14 15:39 Christoph Berg <myon@debian.org>
parent: Quan Zongliang <quanzongliang@yeah.net>
0 siblings, 1 reply; 19+ messages in thread
From: Christoph Berg @ 2025-03-14 15:39 UTC (permalink / raw)
To: Quan Zongliang <quanzongliang@yeah.net>; +Cc: PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Re: Quan Zongliang
> According to the statvfs documentation for macOS
> f_frsize The size in bytes of the minimum unit of allocation on this
> file system.
> f_bsize The preferred length of I/O requests for files on this file
> system.
Thanks for catching that. f_frsize is the correct field to use. The
statvfs(3) manpage on Linux has it as well, but it's less pronounced
there so I missed it:
struct statvfs {
unsigned long f_bsize; /* Filesystem block size */
unsigned long f_frsize; /* Fragment size */
fsblkcnt_t f_blocks; /* Size of fs in f_frsize units */
fsblkcnt_t f_bfree; /* Number of free blocks */
fsblkcnt_t f_bavail; /* Number of free blocks for
unprivileged users */
> In addition, many systems use 1000 as 1k to represent the storage size.
> Shouldn't we consider this factor as well?
That would be a different pg_size_pretty() function, unrelated to this
patch.
I'm still unconvinced if we should use statfs() instead of statvfs()
on *BSD or if their manpage is just trolling us and statvfs is just
fine.
DESCRIPTION
The statvfs() and fstatvfs() functions fill the structure pointed to by
buf with garbage. This garbage will occasionally bear resemblance to
file system statistics, but portable applications must not depend on
this.
Christoph
Attachments:
[text/x-diff] v3-0001-Add-pg_tablespace_avail-functions.patch (10.2K, ../../Z9RNywZyd364KcZL@msg.df7cb.de/2-v3-0001-Add-pg_tablespace_avail-functions.patch)
download | inline diff:
From df4ce715ff91bf095de94ee374fff0ebe9c1d4de Mon Sep 17 00:00:00 2001
From: Christoph Berg <myon@debian.org>
Date: Fri, 14 Mar 2025 16:29:19 +0100
Subject: [PATCH] Add pg_tablespace_avail() functions
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered 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
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2025-03-15 01:04 Thomas Munro <thomas.munro@gmail.com>
parent: Christoph Berg <myon@debian.org>
0 siblings, 1 reply; 19+ messages in thread
From: Thomas Munro @ 2025-03-15 01:04 UTC (permalink / raw)
To: Christoph Berg <myon@debian.org>; +Cc: Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Sat, Mar 15, 2025 at 4:40 AM Christoph Berg <myon@debian.org> wrote:
> I'm still unconvinced if we should use statfs() instead of statvfs()
> on *BSD or if their manpage is just trolling us and statvfs is just
> fine.
>
> DESCRIPTION
> The statvfs() and fstatvfs() functions fill the structure pointed to by
> buf with garbage. This garbage will occasionally bear resemblance to
> file system statistics, but portable applications must not depend on
> this.
Hah, I see this in my local FreeBSD man page. I guess this might be a
reference to POSIX's 100% get-out clause "it is unspecified whether
all members of the statvfs structure have meaningful values on all
file systems". The statfs() man page doesn't say that (a nonstandard
syscall that originated in 4.4BSD, which POSIX decided to rename
because other systems sprouted incompatible statfs() interfaces?).
It's hard to imagine a system that doesn't track free space and report
it here, and if it doesn't, well so what, that's probably also a
system that can't report free space to the "df" command, so what are
we supposed to do? We could perhaps add a note to the documentation
that this field relies on the OS providing meaningful "avail" field in
statvfs(), but it's hard to imagine. Maybe just defer that until
someone shows up with a real report? So +1 from me, go for it, call
statvfs() and don't worry.
I tried your v3 patch on my FreeBSD 14.2 battle station:
postgres=# \db+
List of tablespaces
Name | Owner | Location | Access privileges | Options | Size
| Free | Description
------------+--------+----------+-------------------+---------+--------+--------+-------------
pg_default | tmunro | | | | 22 MB
| 290 GB |
pg_global | tmunro | | | | 556 kB
| 290 GB |
That is the correct answer:
tmunro@build1:~/projects/postgresql/build $ df -h .
Filesystem Size Used Avail Capacity Mounted on
zroot/usr/home 331G 41G 290G 12% /usr/home
I also pushed your patch to CI and triggered the NetBSD and OpenBSD
tasks and they passed your sanity test, though that only checks that
the reported some number > 1MB.
I looked at the source, and on FreeBSD statvfs[1] is just a libc
function that calls statfs() (as does df). The statfs() man page has
no funny disclaimers. OpenBSD's[2] too. NetBSD seems to have a real
statvfs (or statvfs1) syscall but its man page has no funny
disclaimers.
+#ifdef WIN32
+ if (GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable,
NULL, NULL) == false)
+ 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
What's the rationale for not raising an error if the system call
fails? If someone complains that it's showing -1, doesn't that mean
we'll have to ask them to trace the system calls to figure out why, or
if it's Windows, likely abandon all hope of ever knowing why? Should
statvfs() retry on EINTR?
Style nit: maybe ! instead of == false?
Nice feature.
[1] https://github.com/freebsd/freebsd-src/blob/36782aaba4f1a7d054aa405357a8fa2bc0f94eb0/lib/libc/gen/st...
[2] https://github.com/openbsd/src/blob/70ab9842eb8b368612eb098db19dcf94c19d673d/lib/libc/gen/statvfs.c#...
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2025-03-15 12:09 Christoph Berg <myon@debian.org>
parent: Thomas Munro <thomas.munro@gmail.com>
0 siblings, 2 replies; 19+ messages in thread
From: Christoph Berg @ 2025-03-15 12:09 UTC (permalink / raw)
To: Thomas Munro <thomas.munro@gmail.com>; +Cc: Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Re: Thomas Munro
> Hah, I see this in my local FreeBSD man page. I guess this might be a
> reference to POSIX's 100% get-out clause "it is unspecified whether
> all members of the statvfs structure have meaningful values on all
> file systems".
Yeah I could hear someone being annoyed by POSIX echoed in that
paragraph.
> system that can't report free space to the "df" command, so what are
> we supposed to do? We could perhaps add a note to the documentation
> that this field relies on the OS providing meaningful "avail" field in
> statvfs(), but it's hard to imagine. Maybe just defer that until
> someone shows up with a real report? So +1 from me, go for it, call
> statvfs() and don't worry.
I was reading looking into gnulib's wrapper around this - it's also
basically calling statvfs() except on assorted older systems.
https://github.com/coreutils/gnulib/blob/master/lib/fsusage.c#L114
Do we care about any of these?
AIX
OSF/1
2.6 < glibc/Linux < 2.6.36
glibc/Linux < 2.6, 4.3BSD, SunOS 4, \
Mac OS X < 10.4, FreeBSD < 5.0, \
NetBSD < 3.0, OpenBSD < 4.4k
SunOS 4.1.2, 4.1.3, and 4.1.3_U1
4.4BSD and older NetBSD
SVR3, old Irix
If not, then statvfs seems safe.
> I also pushed your patch to CI and triggered the NetBSD and OpenBSD
> tasks and they passed your sanity test, though that only checks that
> the reported some number > 1MB.
I thought about making that test "between 1MB and 10PB", but that
seemed silly - it's not testing much, and some day, someone will try
to run the test on a system where it will still fail.
> What's the rationale for not raising an error if the system call
> fails?
That's mirroring the behavior of calculate_tablespace_size() in the
same file. I thought that's to allow \db+ to succeed even if some of
the tablespaces are botched/missing/whatever. But now on closer
inspection, I see that db_dir_size() is erroring out on problems, it
just ignores the top-level directory missing. Fixed in the attached
patch.
\db+
FEHLER: XX000: could not statvfs directory "pg_tblspc/16384/PG_18_202503111": Zu viele Ebenen aus symbolischen Links
LOCATION: calculate_tablespace_avail, dbsize.c:373
But this is actually something I wanted to address in a follow-up
patch: Currently, non-superusers cannot run \db+ because they lack
CREATE on pg_global (but `\db+ pg_default` works). Should we rather
make pg_database_size and pg_database_avail return NULL for
insufficient permissions instead of throwing an error?
> If someone complains that it's showing -1, doesn't that mean
(-1 is translated to NULL for the SQL level.)
> we'll have to ask them to trace the system calls to figure out why, or
> if it's Windows, likely abandon all hope of ever knowing why? Should
> statvfs() retry on EINTR?
Hmm. Is looping on EINTR worth the trouble?
> Style nit: maybe ! instead of == false?
Changed.
> Nice feature.
Thanks!
Christoph
Attachments:
[text/x-diff] v4-0001-Add-pg_tablespace_avail-functions.patch (10.4K, ../../Z9Vt6QYesJry7209@msg.df7cb.de/2-v4-0001-Add-pg_tablespace_avail-functions.patch)
download | inline diff:
From db42fdcc5ee3097b3364ec51602d6d58994b2060 Mon Sep 17 00:00:00 2001
From: Christoph Berg <myon@debian.org>
Date: Fri, 14 Mar 2025 16:29:19 +0100
Subject: [PATCH v4] Add pg_tablespace_avail() functions
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered 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
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2025-03-15 12:17 Laurenz Albe <laurenz.albe@cybertec.at>
parent: Christoph Berg <myon@debian.org>
1 sibling, 1 reply; 19+ messages in thread
From: Laurenz Albe @ 2025-03-15 12:17 UTC (permalink / raw)
To: Christoph Berg <myon@debian.org>; Thomas Munro <thomas.munro@gmail.com>; +Cc: Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Sat, 2025-03-15 at 13:09 +0100, Christoph Berg wrote:
> Do we care about any of these?
>
> AIX
We dropped support for it, but there are efforts to change that.
Yours,
Laurenz Albe
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2025-03-15 13:15 Thomas Munro <thomas.munro@gmail.com>
parent: Laurenz Albe <laurenz.albe@cybertec.at>
0 siblings, 0 replies; 19+ messages in thread
From: Thomas Munro @ 2025-03-15 13:15 UTC (permalink / raw)
To: Laurenz Albe <laurenz.albe@cybertec.at>; +Cc: Christoph Berg <myon@debian.org>; Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Sun, Mar 16, 2025 at 1:17 AM Laurenz Albe <laurenz.albe@cybertec.at> wrote:
> On Sat, 2025-03-15 at 13:09 +0100, Christoph Berg wrote:
> > Do we care about any of these?
> >
> > AIX
>
> We dropped support for it, but there are efforts to change that.
FWIW AIX does have it, according to its manual, in case it comes back.
The others in the list are defunct or obsolete versions.
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2025-03-15 13:24 Thomas Munro <thomas.munro@gmail.com>
parent: Christoph Berg <myon@debian.org>
1 sibling, 1 reply; 19+ messages in thread
From: Thomas Munro @ 2025-03-15 13:24 UTC (permalink / raw)
To: Christoph Berg <myon@debian.org>; +Cc: Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Sun, Mar 16, 2025 at 1:09 AM Christoph Berg <myon@debian.org> wrote:
> Hmm. Is looping on EINTR worth the trouble?
I was just wondering if it might be one of those oddballs that ignores
SA_RESTART, but I guess that doesn't seem too likely (I mean, first
you'd probably have to have a reason to sleep or some other special
reason, and who knows what some unusual file systems might do). It
certainly doesn't on the systems I tried. So I guess not until we
have other evidence.
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2025-03-15 17:00 Christoph Berg <myon@debian.org>
parent: Thomas Munro <thomas.munro@gmail.com>
0 siblings, 1 reply; 19+ messages in thread
From: Christoph Berg @ 2025-03-15 17:00 UTC (permalink / raw)
To: Thomas Munro <thomas.munro@gmail.com>; +Cc: Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Re: Thomas Munro
> > Hmm. Is looping on EINTR worth the trouble?
>
> I was just wondering if it might be one of those oddballs that ignores
> SA_RESTART, but I guess that doesn't seem too likely (I mean, first
> you'd probably have to have a reason to sleep or some other special
> reason, and who knows what some unusual file systems might do). It
> certainly doesn't on the systems I tried. So I guess not until we
> have other evidence.
Gnulib's get_fs_usage() (which is what GNU coreutil's df uses)
does not handle EINTR either.
There is some code that does int width expansion, but I believe we
don't need that since the `fst.f_bavail * fst.f_frsize` multiplication
takes care of converting that to int64 (if it wasn't already 64bits
before).
Christoph
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2025-04-24 19:26 said assemlal <oyoun@gmx.com>
parent: Christoph Berg <myon@debian.org>
1 sibling, 0 replies; 19+ messages in thread
From: said assemlal @ 2025-04-24 19:26 UTC (permalink / raw)
To: Christoph Berg <myon@debian.org>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Hi,
I also tested the patch on Linux mint 22.1 with the btrfs and ext4
partitions. I generated some data and the outcome looks good:
postgres=# \db+
List of tablespaces
Name | Owner | Location | Access
privileges | Options | Size | Free | Description
------------------+----------+---------------------------+-------------------+---------+---------+---------+-------------
pg_default | postgres | | | | 1972 MB
| 29 GB |
pg_global | postgres | | | | 556 kB
| 29 GB |
tablespace_test2 | postgres | /media/said/queryme/pgsql
| | | 3147 MB | 1736 GB |
Numbers are the same as if I were executing the command: df -h
tablespace_test2 was the ext4 partition on usb stick.
Numbers are correct.
Said
On 2025-03-13 14 h 10, Christoph Berg wrote:
> Hi,
>
> I'm picking up a 5 year old patch again:
> https://www.postgresql.org/message-id/flat/20191108132419.GG8017%40msg.df7cb.de
>
> Users will be interested in knowing how much extra data they can load
> into a database, but PG currently does not expose that number. This
> patch introduces a new function pg_tablespace_avail() that takes a
> tablespace name or oid, and returns the number of bytes "available"
> there. This is the number without any reserved blocks (Unix, f_avail)
> or available to the current user (Windows).
>
> (This is not meant to replace a full-fledged OS monitoring system that
> has much more numbers about disks and everything, it is filling a UX
> gap.)
>
> Compared to the last patch, this just returns a single number so it's
> easier to use - total space isn't all that interesting, we just return
> the number the user wants.
>
> The free space is included in \db+ output:
>
> postgres =# \db+
> List of tablespaces
> Name │ Owner │ Location │ Access privileges │ Options │ Size │ Free │ Description
> ────────────┼───────┼──────────┼───────────────────┼─────────┼─────────┼────────┼─────────────
> pg_default │ myon │ │ ∅ │ ∅ │ 23 MB │ 538 GB │ ∅
> pg_global │ myon │ │ ∅ │ ∅ │ 556 kB │ 538 GB │ ∅
> spc │ myon │ /tmp/spc │ ∅ │ ∅ │ 0 bytes │ 31 GB │ ∅
> (3 rows)
>
> The patch has also been tested on Windows.
>
> TODO: Figure out which systems need statfs() vs statvfs()
>
> Christoph
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2026-05-05 17:54 Christoph Berg <myon@debian.org>
parent: Christoph Berg <myon@debian.org>
0 siblings, 2 replies; 19+ messages in thread
From: Christoph Berg @ 2026-05-05 17:54 UTC (permalink / raw)
To: Thomas Munro <thomas.munro@gmail.com>; +Cc: Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
I'm picking this up again. Attached is version 5 of the
pg_tablespace_avail() patch.
Difference to v4 is that the \db+ query used in psql is now checking
tablespace permissions before blindly calling the function. This
avoids raising errors when some tablespace is not accessible.
postgres =# \db+
/**** INTERNAL QUERY ****/
/* Get matching tablespaces */
SELECT spcname AS "Name",
pg_catalog.pg_get_userbyid(spcowner) AS "Owner",
pg_catalog.pg_tablespace_location(tblspc.oid) AS "Location",
CASE WHEN pg_catalog.array_length(spcacl, 1) = 0 THEN '(none)' ELSE pg_catalog.array_to_string(spcacl, E'\n') END AS "Access privileges",
spcoptions AS "Options",
CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR
pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR
pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')
THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(tblspc.oid))
ELSE 'No Access' END as "Size",
CASE WHEN dbsub.dattablespace OPERATOR(pg_catalog.=) tblspc.oid OR
pg_catalog.has_tablespace_privilege(tblspc.oid, 'CREATE') OR
pg_catalog.pg_has_role('pg_read_all_stats', 'USAGE')
THEN pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_avail(tblspc.oid))
ELSE 'No Access' END as "Free",
pg_catalog.shobj_description(tblspc.oid, 'pg_tablespace') AS "Description"
FROM pg_catalog.pg_tablespace tblspc
CROSS JOIN (SELECT dattablespace FROM pg_catalog.pg_database db
wHERE db.datname OPERATOR(pg_catalog.=) pg_catalog.current_database()) dbsub
ORDER BY 1;
/************************/
The logic is the same as in pg_tablespace_size (which wasn't guarded in psql before):
* this database's default tablespace is ok
* having CREATE is ok
* rold pg_read_all_stats is ok
List of tablespaces
Name │ Owner │ Location │ Access privileges │ Options │ Size │ Free │ Description
────────────┼───────┼──────────┼───────────────────┼─────────┼────────┼────────┼─────────────
pg_default │ myon │ │ ∅ │ ∅ │ 24 MB │ 365 GB │ ∅
pg_global │ myon │ │ ∅ │ ∅ │ 549 kB │ 365 GB │ ∅
(2 rows)
I think this patch is useful as-is and could be committed.
As a followup, I would like to include pg_wal in this list since it
can be moved to a separate disk. There are several ways forward:
1) include a pg_wal entry in pg_tablespace. Together with a trivial
addition to get_tablespace_location:
+ if (tablespaceOid == WALTABLESPACE_OID)
+ snprintf(sourcepath, sizeof(sourcepath), "%s", XLOGDIR);
this makes the \db+ query report size/free out of the box. This
seemed very clean to me until I discovered the downside that it
required not-so-trivial guarding against WALTABLESPACE_OID being
used as tablespace in SQL commands in many code places.
2) add new pg_wal_size() and pg_wal_avail() functions
3) reserve a special value that makes a combination of
get_tablespace_location, pg_tablespace_size and pg_tablespace_avail
work on pg_wal even when that's not registered in pg_tablespace.
Not sure what way is best, perhaps something between 2 and 3?
Christoph
Attachments:
[text/x-diff] v5-0001-Add-pg_tablespace_avail-functions.patch (12.5K, ../../afouz5FNChXsSDmw@msg.df7cb.de/2-v5-0001-Add-pg_tablespace_avail-functions.patch)
download | inline diff:
From fb7219266cee61f278dc8236c27ed63992cb0422 Mon Sep 17 00:00:00 2001
From: Christoph Berg <myon@debian.org>
Date: Fri, 14 Mar 2025 16:29:19 +0100
Subject: [PATCH v5] Add pg_tablespace_avail() functions
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_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
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2026-05-06 22:59 Zsolt Parragi <zsolt.parragi@percona.com>
parent: Christoph Berg <myon@debian.org>
1 sibling, 1 reply; 19+ messages in thread
From: Zsolt Parragi @ 2026-05-06 22:59 UTC (permalink / raw)
To: Christoph Berg <myon@debian.org>; +Cc: Thomas Munro <thomas.munro@gmail.com>; Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Hello!
#ifdef WIN32
+ if (! GetDiskFreeSpaceEx(tblspcPath, &lpFreeBytesAvailable, NULL, NULL))
+ elog(ERROR, "GetDiskFreeSpaceEx failed: error code %lu", GetLastError());
+
+ return lpFreeBytesAvailable.QuadPart; /* ULONGLONG part of ULARGE_INTEGER */
+#else
Shouldn't this use proper error codes similar to the else branch, and
also _dosmaperr?
There's also a behavior difference here compared to Linux, it returns
-1 on ENOENT, the Windows version errors out on the matching
condition.
+ " wHERE db.datname OPERATOR(pg_catalog.=)
pg_catalog.current_database()) dbsub\n");
typo, should be WHERE
+ (errcode_for_file_access(),
+ errmsg("could not statvfs directory \"%s\": %m", tblspcPath)));
Is this error message user friendly? Wouldn't be something like "could
not get free disk space for directory" be better?
+ Returns the available disk space in the tablespace with the
+ specified name or OID.
Does the tablespace have a disk space? Maybe "returns the space on the
filesystem hosting the tablespace"?
+ return fst.f_bavail * fst.f_frsize; /* available blocks times fragment size */
> There is some code that does int width expansion, but I believe we
> don't need that since the `fst.f_bavail * fst.f_frsize` multiplication
> takes care of converting that to int64 (if it wasn't already 64bits
> before).
I don't think this is the case, we first multiply and then cast.
Multiplication still happens with 32 bit types.
Relevant parts on Godbolt: https://godbolt.org/z/7dj7crf6K
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2026-05-22 06:15 solai v <solai.cdac@gmail.com>
parent: Christoph Berg <myon@debian.org>
1 sibling, 1 reply; 19+ messages in thread
From: solai v @ 2026-05-22 06:15 UTC (permalink / raw)
To: Christoph Berg <myon@debian.org>; +Cc: Thomas Munro <thomas.munro@gmail.com>; Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Hi ,
I tested the v5 of the pg_tablespace_avail() patch on Linux.
The patch applied and built cleanly for me .After applying the patch
and re-running initdb,pg_tablespace_avail() worked correctly and \db+
showed the new Free column as expected.
The reported values matched the output from df -h on my system.I also
tested custom tablespace and non-superuser access,and both behaved
correctly.
Additionally,I ran :
make check TESTS= tablespace
and all tests passed.
Overall , the feature looks useful and worked well in my testing.
Regards
Solai
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2026-07-17 06:38 Rafia Sabih <rafia.pghackers@gmail.com>
parent: solai v <solai.cdac@gmail.com>
0 siblings, 0 replies; 19+ messages in thread
From: Rafia Sabih @ 2026-07-17 06:38 UTC (permalink / raw)
To: solai v <solai.cdac@gmail.com>; +Cc: Christoph Berg <myon@debian.org>; Thomas Munro <thomas.munro@gmail.com>; Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
On Fri, 22 May 2026 at 11:46, solai v <solai.cdac@gmail.com> wrote:
> Hi ,
> I tested the v5 of the pg_tablespace_avail() patch on Linux.
> The patch applied and built cleanly for me .After applying the patch
> and re-running initdb,pg_tablespace_avail() worked correctly and \db+
> showed the new Free column as expected.
> The reported values matched the output from df -h on my system.I also
> tested custom tablespace and non-superuser access,and both behaved
> correctly.
> Additionally,I ran :
> make check TESTS= tablespace
> and all tests passed.
> Overall , the feature looks useful and worked well in my testing.
>
> Regards
> Solai
>
>
> I looked into this patch and here are my two cents, the change in
describe.c is in if (pset.sversion >= 190000), I think it should be
pset.sversion >= 200000 now, isn't it...?
Also, the first part of calculate_tablespace_avail and
calculate_tablespace_size are the same, would it make sense to have a
separate routine to have the acl check...?
--
Regards,
Rafia Sabih
CYBERTEC PostgreSQL International GmbH
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2026-07-21 13:09 Christoph Berg <myon@debian.org>
parent: Zsolt Parragi <zsolt.parragi@percona.com>
0 siblings, 2 replies; 19+ messages in thread
From: Christoph Berg @ 2026-07-21 13:09 UTC (permalink / raw)
To: Zsolt Parragi <zsolt.parragi@percona.com>; solai v <solai.cdac@gmail.com>; Rafia Sabih <rafia.pghackers@gmail.com>; +Cc: Thomas Munro <thomas.munro@gmail.com>; Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Zolt, Solai, Rafia,
thanks for the reviews!
Re: Zsolt Parragi
> Shouldn't this use proper error codes similar to the else branch, and
> also _dosmaperr?
>
> There's also a behavior difference here compared to Linux, it returns
> -1 on ENOENT, the Windows version errors out on the matching
> condition.
> typo, should be WHERE
> Is this error message user friendly? Wouldn't be something like "could
> not get free disk space for directory" be better?
> Does the tablespace have a disk space? Maybe "returns the space on the
> filesystem hosting the tablespace"?
All fixed.
> I don't think this is the case, we first multiply and then cast.
> Multiplication still happens with 32 bit types.
Thanks for catching that, also fixed!
Re: solai v
> Overall , the feature looks useful and worked well in my testing.
Thanks!
Re: Rafia Sabih
> > I looked into this patch and here are my two cents, the change in
> describe.c is in if (pset.sversion >= 190000), I think it should be
> pset.sversion >= 200000 now, isn't it...?
Yeah, PG 20 didn't exist yet when I wrote it. Updated now, thanks!
> Also, the first part of calculate_tablespace_avail and
> calculate_tablespace_size are the same, would it make sense to have a
> separate routine to have the acl check...?
We could merge the two functions into one and then add an if block for
the main function part, but I think it would make it messy.
The common part is the acl check plus the setup of the tblspcPath
variable, so we'd probably need two extra functions, but they would be
very small. I think we should leave it like it is now, but I'm open to
ideas of course.
v6 attached.
Christoph
Attachments:
[text/x-diff] v6-0001-Add-pg_tablespace_avail-functions.patch (12.7K, ../../al9vlU6MqdO2GNu5@msg.df7cb.de/2-v6-0001-Add-pg_tablespace_avail-functions.patch)
download | inline diff:
From 82a319424635445798b03ccbdc17d2aecbfd676f Mon Sep 17 00:00:00 2001
From: Christoph Berg <myon@debian.org>
Date: Tue, 21 Jul 2026 15:08:57 +0200
Subject: [PATCH v6] Add pg_tablespace_avail() functions
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_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
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2026-07-21 19:37 Zsolt Parragi <zsolt.parragi@percona.com>
parent: Christoph Berg <myon@debian.org>
1 sibling, 1 reply; 19+ messages in thread
From: Zsolt Parragi @ 2026-07-21 19:37 UTC (permalink / raw)
To: Christoph Berg <myon@debian.org>; +Cc: pgsql-hackers@lists.postgresql.org, Thomas Munro <thomas.munro@gmail.com>
@@ -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"
While I think this is a good change, it isn't mentioned in the commit
message and seems somewhat unrelated to the other changes?
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2026-07-22 11:30 Christoph Berg <myon@debian.org>
parent: Zsolt Parragi <zsolt.parragi@percona.com>
0 siblings, 0 replies; 19+ messages in thread
From: Christoph Berg @ 2026-07-22 11:30 UTC (permalink / raw)
To: Zsolt Parragi <zsolt.parragi@percona.com>; +Cc: pgsql-hackers@lists.postgresql.org, Thomas Munro <thomas.munro@gmail.com>
Re: Zsolt Parragi
> While I think this is a good change, it isn't mentioned in the commit
> message and seems somewhat unrelated to the other changes?
You are right, I remember spotting that along the way and should have
submitted it separately. Done now, thanks!
v7 attached without that change.
Christoph
Attachments:
[text/x-diff] v7-0001-Add-pg_tablespace_avail-functions.patch (12.2K, ../../amCpy7Kjqjein32f@msg.df7cb.de/2-v7-0001-Add-pg_tablespace_avail-functions.patch)
download | inline diff:
From cf0974e086f73831dbd0f52e08af7fddb905a754 Mon Sep 17 00:00:00 2001
From: Christoph Berg <myon@debian.org>
Date: Tue, 21 Jul 2026 15:08:57 +0200
Subject: [PATCH v7] Add pg_tablespace_avail() functions
This exposes the f_avail value from statvfs() on tablespace directories
on the SQL level, allowing monitoring of free disk space from within the
server. On windows, GetDiskFreeSpaceEx() is used.
Permissions required match those from pg_tablespace_size().
In psql, include a new "Free" column in \db+ output.
Add test coverage for pg_tablespace_avail() and the previously not
covered pg_tablespace_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
^ permalink raw reply [nested|flat] 19+ messages in thread
* Re: Available disk space per tablespace
@ 2026-07-28 09:35 solai v <solai.cdac@gmail.com>
parent: Christoph Berg <myon@debian.org>
1 sibling, 0 replies; 19+ messages in thread
From: solai v @ 2026-07-28 09:35 UTC (permalink / raw)
To: Christoph Berg <myon@debian.org>; +Cc: Zsolt Parragi <zsolt.parragi@percona.com>; Rafia Sabih <rafia.pghackers@gmail.com>; Thomas Munro <thomas.munro@gmail.com>; Quan Zongliang <quanzongliang@yeah.net>; PostgreSQL Hackers <pgsql-hackers@lists.postgresql.org>
Hi Christoph,
I tested the latest v7 patch on Linux.
The patch applied and built without any issues. After rebuilding
PostgreSQL and re-running initdb, I verified the new functionality.
I tested both the name and oid variants of pg_tablespace_avail(),
confirmed that the new Free column in \db+ displays the expected
values, and compared the reported free space with df -h, which matched
as expected. I also created a custom tablespace and verified that it
reports the available space correctly.
In addition, I tested the behavior with a non-superuser account to
verify the permission handling, and everything worked as expected.
Finally, I ran:
make check TESTS=tablespace
The tablespace regression test passed successfully, and I also ran the
full regression test suite, which completed with all tests passing.
Overall, the v7 changes worked well in my testing, and I didn't
encounter any issues.
Regards,
Solai
^ permalink raw reply [nested|flat] 19+ messages in thread
end of thread, other threads:[~2026-07-28 09:35 UTC | newest]
Thread overview: 19+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2019-09-16 12:36 [PATCH 2/5] Use IndexOrderByDistance in SP-GiST user functions Nikita Glukhov <n.gluhov@postgrespro.ru>
2025-03-13 18:10 Available disk space per tablespace Christoph Berg <myon@debian.org>
2025-03-14 02:14 ` Re: Available disk space per tablespace Quan Zongliang <quanzongliang@yeah.net>
2025-03-14 15:39 ` Re: Available disk space per tablespace Christoph Berg <myon@debian.org>
2025-03-15 01:04 ` Re: Available disk space per tablespace Thomas Munro <thomas.munro@gmail.com>
2025-03-15 12:09 ` Re: Available disk space per tablespace Christoph Berg <myon@debian.org>
2025-03-15 12:17 ` Re: Available disk space per tablespace Laurenz Albe <laurenz.albe@cybertec.at>
2025-03-15 13:15 ` Re: Available disk space per tablespace Thomas Munro <thomas.munro@gmail.com>
2025-03-15 13:24 ` Re: Available disk space per tablespace Thomas Munro <thomas.munro@gmail.com>
2025-03-15 17:00 ` Re: Available disk space per tablespace Christoph Berg <myon@debian.org>
2026-05-05 17:54 ` Re: Available disk space per tablespace Christoph Berg <myon@debian.org>
2026-05-06 22:59 ` Re: Available disk space per tablespace Zsolt Parragi <zsolt.parragi@percona.com>
2026-07-21 13:09 ` Re: Available disk space per tablespace Christoph Berg <myon@debian.org>
2026-07-21 19:37 ` Re: Available disk space per tablespace Zsolt Parragi <zsolt.parragi@percona.com>
2026-07-22 11:30 ` Re: Available disk space per tablespace Christoph Berg <myon@debian.org>
2026-07-28 09:35 ` Re: Available disk space per tablespace solai v <solai.cdac@gmail.com>
2026-05-22 06:15 ` Re: Available disk space per tablespace solai v <solai.cdac@gmail.com>
2026-07-17 06:38 ` Re: Available disk space per tablespace Rafia Sabih <rafia.pghackers@gmail.com>
2025-04-24 19:26 ` Re: Available disk space per tablespace said assemlal <oyoun@gmx.com>
This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox